package sk.uniza.fri.cerviix.client.network;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import sk.uniza.fri.cerviix.client.Client;
import sk.uniza.fri.cerviix.client.network.message.server.ConnectResponse;
import sk.uniza.fri.cerviix.client.network.message.server.GameEndMessage;
import sk.uniza.fri.cerviix.client.network.message.server.GameStartMessage;
import sk.uniza.fri.cerviix.client.network.message.server.PlayerAddMessage;
import sk.uniza.fri.cerviix.client.network.message.server.PlayerRemoveMessage;
import sk.uniza.fri.cerviix.client.network.message.server.UpdateMessage;

public class NetworkManager
{
	private static final int port = 19191;
	private Socket socket;
	
	private Input input;
	private Output output;
	
	private boolean connected = false;
	
	private Queue<ServerMessage> serverMessages = new LinkedList<ServerMessage>();
	
	public void connect(String address) throws IOException
	{
		socket = new Socket(address, port);
		
		input = new Input(new DataInputStream(socket.getInputStream()));
		output = new Output(new DataOutputStream(socket.getOutputStream()));

		input.register(MessageType.CONNECT, ConnectResponse.class);
		input.register(MessageType.GAME_START, GameStartMessage.class);
		input.register(MessageType.GAME_FINISH, GameEndMessage.class);
		input.register(MessageType.PLAYER_ADD, PlayerAddMessage.class);
		input.register(MessageType.PLAYER_REMOVE, PlayerRemoveMessage.class);
		input.register(MessageType.UPDATE, UpdateMessage.class);
		
		input.start();
		output.start();
		
		connected = true;
	}
	
	public void disconnect()
	{
		if (!connected)
		{
			return;
		}
		
		input.interrupt();
		output.interrupt();
		
		try
		{
			socket.close();
		}
		catch (IOException e)
		{
			Client.log.warning("Socket close exception. Probably closed.");
		}
	}
	
	private synchronized void ioThreadCrash()
	{
		Client.log.warning("Network thread crashed.");
		
		disconnect();
	}
	
	public boolean isConnected()
	{
		if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown())
		{
			return false;
		}
		else
		{
			return true;
		}
	}
	
	private synchronized void receiveMessage(ServerMessage message)
	{
		serverMessages.add(message);
	}
	
	public synchronized boolean hasMessage()
	{
		return (serverMessages.isEmpty()) ? false : true;
	}
	
	public synchronized ServerMessage getMessage()
	{
		return serverMessages.remove();
	}
	
	public void sendMessage(ClientMessage message)
	{
		output.send(message);
	}
	
	private class Input extends Thread
	{
		private DataInputStream stream;
		
		private Map<Byte, Class<? extends ServerMessage>> messageClasses = new HashMap<Byte, Class<? extends ServerMessage>>();
		
		public Input(DataInputStream stream)
		{
			this.stream = stream;
		}

		public void register(MessageType type, Class<? extends ServerMessage> messageClass)
		{
			messageClasses.put(type.value, messageClass);
		}
		
		@Override
		public void run()
		{
			Client.log.info("Input thread running.");
			
			while (true)
			{
				try
				{
					byte type = stream.readByte();
					
					if (!messageClasses.containsKey(type))
					{
						Client.log.severe(String.format("ServerMessage not registered: %d", type));
						
						ioThreadCrash();
					}
					
					try
					{
						Class<? extends ServerMessage> messageClass = messageClasses.get(type);
						ServerMessage message = messageClass.getConstructor().newInstance();
						
						message.attachStream(stream);
						message.receive();
						
						receiveMessage(message);
					}
					catch (IOException e)
					{
						throw e;
					}
					catch (Exception e)
					{
						throw new RuntimeException("Can't instantiate ServerMessage.");
					}
				}
				catch (IOException e)
				{
					if (!isInterrupted())
					{
						Client.log.info("Input thread crashed.");
						ioThreadCrash();
					}
					
					break;
				}
			}
			
			Client.log.info("Input thread stopped.");
		}
	}
	
	private class Output extends Thread
	{
		private DataOutputStream stream;
		
		private BlockingQueue<ClientMessage> messages = new LinkedBlockingQueue<ClientMessage>();
		
		public Output(DataOutputStream stream)
		{
			this.stream = stream;
		}
		
		public void send(ClientMessage message)
		{
			messages.add(message);
		}
		
		@Override
		public void run()
		{
			Client.log.info("Output thread running.");
			
			while (true)
			{
				try
				{
					ClientMessage message = messages.take();
					
					message.attachStream(stream);
					message.send();
				}
				catch (InterruptedException e)
				{
					break;
				}
				catch (IOException e)
				{
					Client.log.info("Output thread crashed.");
					
					ioThreadCrash();
				}
			}
			
			Client.log.info("Output thread stopped.");
		}
	}
}
